﻿Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Documents
Imports System.Windows.Input
Imports System.Windows.Media
Imports System.Windows.Media.Imaging
Imports System.Windows.Navigation
Imports System.Windows.Shapes
Imports System.Drawing
Imports System.Windows.Threading
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports System.Windows.Interop
Imports System.IO
Imports System.Threading

Public Class AnimatedImage
    Inherits System.Windows.Controls.Image
    Private _BitmapSources As List(Of BitmapSource) = Nothing
    Private _nCurrentFrame As Integer = 0
    Private Delays As List(Of Integer) = Nothing
    Private timer As Timer = Nothing

    Private _bIsAnimating As Boolean = False

    Public ReadOnly Property IsAnimating() As Boolean
        Get
            Return _bIsAnimating
        End Get
    End Property

    Shared Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(AnimatedImage), New FrameworkPropertyMetadata(GetType(AnimatedImage)))
    End Sub

    ''' <summary>
    ''' Animated GIF image.
    ''' </summary>
    Public Property AnimatedBitmap() As Bitmap
        Get
            Return DirectCast(GetValue(AnimatedBitmapProperty), Bitmap)
        End Get
        Set(value As Bitmap)
            StopAnimate()
            SetValue(AnimatedBitmapProperty, value)
        End Set
    End Property

    Public Shared ReadOnly AnimatedBitmapProperty As DependencyProperty = DependencyProperty.Register("AnimatedBitmap", GetType(Bitmap), GetType(AnimatedImage), New FrameworkPropertyMetadata(Nothing, New PropertyChangedCallback(AddressOf OnAnimatedBitmapChanged)))

    Private Shared Sub OnAnimatedBitmapChanged(obj As DependencyObject, args As DependencyPropertyChangedEventArgs)
        Dim control As AnimatedImage = DirectCast(obj, AnimatedImage)

        control.UpdateAnimatedBitmap()

        Dim e As New RoutedPropertyChangedEventArgs(Of Bitmap)(DirectCast(args.OldValue, Bitmap), DirectCast(args.NewValue, Bitmap), AnimatedBitmapChangedEvent)
        control.OnAnimatedBitmapChanged(e)
    End Sub

    Public Shared ReadOnly AnimatedBitmapChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("AnimatedBitmapChanged", RoutingStrategy.Bubble, GetType(RoutedPropertyChangedEventHandler(Of Bitmap)), GetType(AnimatedImage))



    Public Custom Event AnimatedBitmapChanged As RoutedPropertyChangedEventHandler(Of Bitmap)

        AddHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Bitmap))

            [AddHandler](AnimatedBitmapChangedEvent, value)

        End AddHandler

        RemoveHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Bitmap))

            [RemoveHandler](AnimatedBitmapChangedEvent, value)

        End RemoveHandler
        RaiseEvent(ByVal sender As System.Object, ByVal e As System.EventArgs)
        End RaiseEvent
    End Event


    ''' <summary>
    ''' Loads the smile out of the bitmap.
    ''' </summary>
    ''' <param name="bitmap">Animated gif file.</param>
    Public Sub LoadSmile(bitmap As Bitmap)
        Me.AnimatedBitmap = bitmap
    End Sub

    ''' <summary>
    ''' Raises the ValueChanged event.
    ''' </summary>
    ''' <param name="args">Arguments associated with the ValueChanged event.</param>
    Protected Overridable Sub OnAnimatedBitmapChanged(args As RoutedPropertyChangedEventArgs(Of Bitmap))
        Try
            [RaiseEvent](args)
        Catch
        End Try
    End Sub

    ''' <summary>
    '''
    ''' </summary>
    Private Sub UpdateAnimatedBitmap()

        Try
            Dim nTimeFrames As Integer = GetFramesCount()
            'get frames count
            _nCurrentFrame = 0
            'Set current frame to default value
            If nTimeFrames > 0 Then
                'this is animated file
                Dim stream As New MemoryStream()
                AnimatedBitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Gif)
                stream.Seek(0, SeekOrigin.Begin)
                Dim buffer As Byte() = New Byte(stream.Length - 1) {}
                stream.Read(buffer, 0, buffer.Length)
                ParseGif(buffer)
                _BitmapSources = New List(Of BitmapSource)(nTimeFrames)
                stream.Dispose()
                FillBitmapSources(nTimeFrames)
                timer = New Timer(AddressOf OnFrameChanged, Nothing, -1, -1)
                'Initialize timer
                'start animation
                StartAnimate()
            Else
                'this is single image
                Dim bitmap As New Bitmap(AnimatedBitmap)
                _BitmapSources = New List(Of BitmapSource)(1)
                _BitmapSources.Add(CreateBitmapSourceFromBitmap(bitmap))
                Source = _BitmapSources(0)
            End If
        Catch
        End Try
    End Sub

    ''' <summary>
    ''' Parse gif file to get delay for each frame
    ''' </summary>
    ''' <param name="buffer"></param>
    Private Sub ParseGif(buffer As Byte())
        Dim p_gif As New ParseGif()
        Delays = p_gif.ParseGifDataStream(buffer, 0)
    End Sub

    ''' <summary>
    ''' Fills the list of GIF frames.
    ''' </summary>
    ''' <param name="nTimeFrames"></param>
    Private Sub FillBitmapSources(nTimeFrames As Integer)
        For i As Integer = 0 To nTimeFrames - 1
            AnimatedBitmap.SelectActiveFrame(System.Drawing.Imaging.FrameDimension.Time, i)
            Dim bitmap As New Bitmap(AnimatedBitmap)
            bitmap.MakeTransparent()
            _BitmapSources.Add(CreateBitmapSourceFromBitmap(bitmap))
            _BitmapSources(i).Freeze()
        Next
    End Sub

    ''' <summary>
    ''' Returns frames count.
    ''' </summary>
    ''' <returns></returns>
    Private Function GetFramesCount() As Integer
        Try
            Return AnimatedBitmap.GetFrameCount(System.Drawing.Imaging.FrameDimension.Time)
        Catch generatedExceptionName As Exception
            Return 1
        End Try
    End Function

    <DllImport("gdi32.dll")> _
    Private Shared Function DeleteObject(hObject As IntPtr) As Boolean
    End Function

    Public Shared Function CreateBitmapSourceFromBitmap(bitmap As System.Drawing.Bitmap) As BitmapSource
        If bitmap Is Nothing Then
            Throw New ArgumentNullException("bitmap")
        End If

        Dim hBitmap As IntPtr = bitmap.GetHbitmap()

        Try
            Return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions())
        Finally
            DeleteObject(hBitmap)
        End Try
    End Function

    Private Delegate Sub VoidDelegate()

    ''' <summary>
    ''' This method is executed on the timer.
    ''' </summary>
    ''' <param name="obj"></param>
    Private Sub OnFrameChanged(obj As Object)
        Try
            Dispatcher.BeginInvoke(DispatcherPriority.Render, New VoidDelegate(Sub()

                                                                                   ChangeSource()

                                                                               End Sub))
        Catch ex As Exception

        End Try
       
    End Sub

    ''' <summary>
    ''' Changes the current GIF frame.
    ''' </summary>
    Private Sub ChangeSource()
        Try
            timer.Change(Delays(_nCurrentFrame) * 10, 0)

            Source = _BitmapSources(_nCurrentFrame)
            _nCurrentFrame = _nCurrentFrame + 1
            _nCurrentFrame = _nCurrentFrame Mod _BitmapSources.Count
        Catch
        End Try
    End Sub

    ''' <summary>
    ''' Stops animation process.
    ''' </summary>
    Public Sub StopAnimate()
        Try
            If _bIsAnimating Then
                timer.Change(-1, -1)
                _bIsAnimating = False
            End If
        Catch
        End Try
    End Sub

    ''' <summary>
    ''' Starts animation process.
    ''' </summary>
    Public Sub StartAnimate()
        Try
            If Not _bIsAnimating Then
                timer.Change(0, 0)
                _bIsAnimating = True
            End If
        Catch
        End Try
    End Sub

    Public Sub Dispose()
        Try
            timer.Change(-1, -1)
            timer.Dispose()
            _BitmapSources.Clear()
            Source = Nothing
            GC.Collect()
            GC.SuppressFinalize(Me)
            GC.WaitForPendingFinalizers()
            GC.Collect()
        Catch
        End Try
    End Sub
End Class


Public Class ParseGif
    Private Delays As New List(Of Integer)()

    Public Function ParseGifDataStream(gifData As Byte(), offset As Integer) As List(Of Integer)
        Delays.Clear()
        offset = ParseHeader(gifData, offset)
        offset = ParseLogicalScreen(gifData, offset)
        While offset <> -1
            offset = ParseBlock(gifData, offset)
        End While
        Return Delays
    End Function

    Private Function ParseHeader(ByRef gifData As Byte(), offset As Integer) As Integer
        Dim str As String = System.Text.ASCIIEncoding.ASCII.GetString(gifData, offset, 3)
        If str <> "GIF" Then
            Throw New FormatException("Not a proper GIF file: missing GIF header")
        End If
        Return 6
    End Function

    Private Function ParseLogicalScreen(ByRef gifData As Byte(), offset As Integer) As Integer
        Dim _logicalWidth As Integer = BitConverter.ToUInt16(gifData, offset)
        Dim _logicalHeight As Integer = BitConverter.ToUInt16(gifData, offset + 2)

        Dim packedField As Byte = gifData(offset + 4)
        Dim hasGlobalColorTable As Boolean = If(CInt(packedField And &H80) > 0, True, False)

        Dim currentIndex As Integer = offset + 7
        If hasGlobalColorTable Then
            Dim colorTableLength As Integer = packedField And &H7
            colorTableLength = CInt(Math.Truncate(Math.Pow(2, colorTableLength + 1))) * 3
            currentIndex = currentIndex + colorTableLength
        End If
        Return currentIndex
    End Function

    Private Function ParseBlock(ByRef gifData As Byte(), offset As Integer) As Integer
        Select Case gifData(offset)
            Case &H21
                If gifData(offset + 1) = &HF9 Then
                    Return ParseGraphicControlExtension(gifData, offset)
                Else
                    Return ParseExtensionBlock(gifData, offset)
                End If
            Case &H2C
                offset = ParseGraphicBlock(gifData, offset)
                Return offset
            Case &H3B
                Return -1
            Case Else
                Throw New FormatException("GIF format incorrect: missing graphic block or special-purpose block. ")
        End Select
    End Function

    Private Function ParseGraphicControlExtension(ByRef gifData As Byte(), offset As Integer) As Integer
        Dim returnOffset As Integer = offset
        ' Extension Block
        Dim length As Integer = gifData(offset + 2)
        returnOffset = offset + length + 2 + 1

        Dim packedField As Byte = gifData(offset + 3)

        ' Get DelayTime
        Dim delay As Integer = BitConverter.ToUInt16(gifData, offset + 4)
        Dim delayTime As Integer = If((delay < 10), 10, delay)
        Delays.Add(delayTime)
        While gifData(returnOffset) <> &H0
            returnOffset = returnOffset + gifData(returnOffset) + 1
        End While

        returnOffset += 1

        Return returnOffset
    End Function

    Private Function ParseExtensionBlock(ByRef gifData As Byte(), offset As Integer) As Integer
        Dim returnOffset As Integer = offset
        ' Extension Block
        Dim length As Integer = gifData(offset + 2)
        returnOffset = offset + length + 2 + 1
        ' check if netscape continousLoop extension
        If gifData(offset + 1) = &HFF AndAlso length > 10 Then
            Dim netscape As String = System.Text.ASCIIEncoding.ASCII.GetString(gifData, offset + 3, 8)
            If netscape = "NETSCAPE" Then
                Dim _numberOfLoops As Integer = BitConverter.ToUInt16(gifData, offset + 16)
                If _numberOfLoops > 0 Then
                    _numberOfLoops += 1
                End If
            End If
        End If
        While gifData(returnOffset) <> &H0
            returnOffset = returnOffset + gifData(returnOffset) + 1
        End While

        returnOffset += 1

        Return returnOffset
    End Function

    Private Function ParseGraphicBlock(ByRef gifData As Byte(), offset As Integer) As Integer
        Dim packedField As Byte = gifData(offset + 9)
        Dim hasLocalColorTable As Boolean = If(CInt(packedField And &H80) > 0, True, False)

        Dim currentIndex As Integer = offset + 9
        If hasLocalColorTable Then
            Dim colorTableLength As Integer = packedField And &H7
            colorTableLength = CInt(Math.Truncate(Math.Pow(2, colorTableLength + 1))) * 3
            currentIndex = currentIndex + colorTableLength
        End If
        currentIndex += 1
        ' Skip 0x00
        currentIndex += 1
        ' Skip LZW Minimum Code Size;
        While gifData(currentIndex) <> &H0
            Dim length As Integer = gifData(currentIndex)
            currentIndex = currentIndex + gifData(currentIndex)
            ' Skip initial size byte
            currentIndex += 1
        End While
        currentIndex = currentIndex + 1
        Return currentIndex
    End Function
End Class


